Skip to content

feat: email reminders for follow-up dates via Brevo - #26

Merged
mankatcheung merged 2 commits into
mainfrom
feat/email-reminders
Jul 20, 2026
Merged

mankatcheung merged 2 commits into
mainfrom
feat/email-reminders

Conversation

@mankatcheung

@mankatcheung mankatcheung commented Jul 20, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Adds `reminderSentAt DateTime?` to `JobApplication` via Prisma migration
  • `IEmailService` port + `BrevoEmailService` (raw `fetch`, no SDK) sending transactional email via Brevo API
  • `findDueForReminder()` + `updateReminderSentAt()` added to `IApplicationRepository`, implemented in `PrismaApplicationRepository`, delegated in `CachedApplicationRepository`
  • `SendFollowUpRemindersUseCase`: queries applications with `followUpAt` in the next 24 h that haven't had a reminder sent, fetches user email, sends via Brevo, marks sent
  • `reminders.plugin.ts`: Fastify plugin running every 1 h via `setInterval`, registered in `app.ts` after container build; clears interval on `onClose`
  • Web: small note on the application detail page when `followUpAt` is set
  • `.env.example`: `BREVO_API_KEY`, `FROM_EMAIL`, `FROM_NAME`

Test plan

  • `pnpm --filter @job-finder/api test` — 192 tests pass including 3 new reminder use-case tests
  • `pnpm --filter @job-finder/api typecheck` — clean
  • `pnpm --filter @job-finder/web typecheck` — clean
  • Set `BREVO_API_KEY` + `FROM_EMAIL` in `.env`, set a `followUpAt` on an application within 24 h from now, restart server and wait for the interval to fire (or call `sendFollowUpRemindersUseCase.execute()` directly in a test script)

Summary by CodeRabbit

  • New Features
    • Added automatic email reminders for upcoming follow-ups, sent 24 hours in advance.
    • Reminders are tracked to prevent duplicate notifications and continue processing if an individual email fails.
    • Added Brevo email configuration options for sending reminders.
  • User Interface
    • Follow-up details now explain that an email reminder will be sent 24 hours before the scheduled date.
  • Bug Fixes
    • Improved reliability when processing reminders for missing accounts or failed email deliveries.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (2)
  • apps/web/src/graphql/generated/gql.ts is excluded by !**/generated/**
  • apps/web/src/graphql/generated/graphql.ts is excluded by !**/generated/**

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cae64162-5918-480b-920d-87bfceb0bd73

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds hourly follow-up reminder processing with Brevo email delivery, persisted reminder timestamps, dependency-injection wiring, shutdown-safe scheduling, database support, tests, and a follow-up reminder notice in the web application view.

Changes

Follow-up reminders

Layer / File(s) Summary
Reminder persistence and repository contracts
apps/api/prisma/*, apps/api/src/domain/application/Application.ts, apps/api/src/use-cases/ports/IApplicationRepository.ts, apps/api/src/infrastructure/db/repositories/*, apps/api/src/__tests__/helpers/*
Adds nullable reminderSentAt storage and repository operations for finding due applications and recording sent reminders.
Email delivery and reminder use case
apps/api/src/use-cases/ports/IEmailService.ts, apps/api/src/infrastructure/email/BrevoEmailService.ts, apps/api/src/use-cases/reminders/*, apps/api/src/__tests__/application/reminders/*, apps/api/src/__tests__/helpers/mocks.ts
Sends formatted follow-up emails through Brevo, skips missing users, continues after individual failures, and records successful sends.
Dependency wiring and hourly scheduling
apps/api/src/http/container.ts, apps/api/src/http/plugins/reminders.plugin.ts, apps/api/src/app.ts, apps/api/.env.example
Registers reminder dependencies, runs the use case hourly, clears the interval during shutdown, and documents Brevo configuration.
Follow-up reminder information
apps/web/src/routes/_authenticated/applications/$applicationId/index.tsx
Displays that an email reminder is sent 24 hours before the follow-up date.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scheduler
  participant ReminderUseCase
  participant ApplicationRepository
  participant EmailService
  Scheduler->>ReminderUseCase: execute()
  ReminderUseCase->>ApplicationRepository: findDueForReminder()
  ReminderUseCase->>EmailService: sendFollowUpReminder()
  ReminderUseCase->>ApplicationRepository: updateReminderSentAt()
Loading

Poem

A rabbit hops through dates so bright,
Sends Brevo letters before night.
The timestamp marks each message sent,
While missed mails do not prevent
The next small hop in the hourly flight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarises the main change: Brevo-based email reminders for follow-up dates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/email-reminders

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Adds a background job that sends follow-up reminder emails 24 h before
a scheduled follow-up date using the Brevo transactional email API.

- reminderSentAt DateTime? column on JobApplication (Prisma + migration)
- IEmailService port + BrevoEmailService (raw fetch, no SDK)
- findDueForReminder() + updateReminderSentAt() on IApplicationRepository
  implemented in PrismaApplicationRepository and delegated in CachedApplicationRepository
- SendFollowUpRemindersUseCase: queries due apps, looks up user email, sends, marks sent
- reminders.plugin.ts: Fastify plugin with 1-hour setInterval, registered in app.ts
- container.ts: emailService (SINGLETON) + sendFollowUpRemindersUseCase (TRANSIENT)
- env.example: BREVO_API_KEY, FROM_EMAIL, FROM_NAME
- Unit tests: sends email, skips missing user, continues past individual failures
- Web: "Email reminder will be sent 24 h before this date" note on detail page
@mankatcheung
mankatcheung force-pushed the feat/email-reminders branch from 6b356a3 to 7e5a868 Compare July 20, 2026 15:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
apps/api/src/use-cases/reminders/SendFollowUpRemindersUseCase.ts (1)

27-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Log errors for failed email deliveries.

While correctly catching the error to prevent the loop from aborting, completely swallowing it reduces observability. Consider logging the error so that delivery failures can be tracked and debugged.

📝 Proposed refactor
-      } catch {
-        // continue — one failure shouldn't block the rest
+      } catch (error) {
+        console.error(`Failed to send reminder for application ${app.id}:`, error);
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/use-cases/reminders/SendFollowUpRemindersUseCase.ts` around
lines 27 - 29, Update the catch block in SendFollowUpRemindersUseCase to log
each failed email delivery with the caught error and relevant delivery context,
while preserving the existing behavior of continuing to process remaining
reminders.
apps/api/src/infrastructure/email/BrevoEmailService.ts (1)

39-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove redundant status code check.

The response.ok property evaluates to true for any status code in the 200-299 range (including 201). If !response.ok is true, response.status is guaranteed not to be 201. The extra check is redundant.

🧹 Proposed simplification
-    if (!response.ok && response.status !== 201) {
+    if (!response.ok) {
       const body = await response.text();
       throw new Error(`Brevo API error ${response.status}: ${body}`);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/infrastructure/email/BrevoEmailService.ts` around lines 39 - 42,
In the response validation within the Brevo email request flow, simplify the
condition to rely solely on response.ok and remove the redundant response.status
!== 201 check. Preserve the existing error body retrieval and Brevo API error
construction for non-OK responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/.env.example`:
- Around line 26-27: Update the FROM_NAME environment variable in the example
configuration to quote its space-containing value, while leaving FROM_EMAIL
unchanged.

In `@apps/api/src/http/plugins/reminders.plugin.ts`:
- Around line 7-14: Update the catch block in the run function to log the caught
error through Fastify’s built-in logger, while preserving the existing behavior
of preventing background reminder failures from crashing the server.

In `@apps/api/src/infrastructure/db/repositories/CachedApplicationRepository.ts`:
- Around line 73-81: Update CachedApplicationRepository.findDueForReminder to
populate the userIdByAppId mapping for every returned application, then update
updateReminderSentAt to use that mapping to invalidate the corresponding
apps:list:${userId}: cache entry in addition to the existing by-id cache
deletion; remove or safely handle the mapping entry after invalidation as
appropriate.

In `@apps/web/src/routes/_authenticated/applications/`$applicationId/index.tsx:
- Around line 192-194: Update the follow-up reminder notice near the
`followUpAt` display to render only when the follow-up date is in the future,
while preserving the existing non-null check. Replace the “24 h before” wording
with language indicating the email reminder is sent during the 24 hours before
the date, consistent with the hourly scheduler behavior.

---

Nitpick comments:
In `@apps/api/src/infrastructure/email/BrevoEmailService.ts`:
- Around line 39-42: In the response validation within the Brevo email request
flow, simplify the condition to rely solely on response.ok and remove the
redundant response.status !== 201 check. Preserve the existing error body
retrieval and Brevo API error construction for non-OK responses.

In `@apps/api/src/use-cases/reminders/SendFollowUpRemindersUseCase.ts`:
- Around line 27-29: Update the catch block in SendFollowUpRemindersUseCase to
log each failed email delivery with the caught error and relevant delivery
context, while preserving the existing behavior of continuing to process
remaining reminders.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bd06653a-199e-4bf3-bdcb-07b4f487d09c

📥 Commits

Reviewing files that changed from the base of the PR and between 65edbb0 and 7e5a868.

📒 Files selected for processing (17)
  • apps/api/.env.example
  • apps/api/prisma/migrations/20260721000001_add_reminder_sent_at/migration.sql
  • apps/api/prisma/schema.prisma
  • apps/api/src/__tests__/application/reminders/SendFollowUpRemindersUseCase.test.ts
  • apps/api/src/__tests__/helpers/createTestDb.ts
  • apps/api/src/__tests__/helpers/mocks.ts
  • apps/api/src/app.ts
  • apps/api/src/domain/application/Application.ts
  • apps/api/src/http/container.ts
  • apps/api/src/http/plugins/reminders.plugin.ts
  • apps/api/src/infrastructure/db/repositories/CachedApplicationRepository.ts
  • apps/api/src/infrastructure/db/repositories/PrismaApplicationRepository.ts
  • apps/api/src/infrastructure/email/BrevoEmailService.ts
  • apps/api/src/use-cases/ports/IApplicationRepository.ts
  • apps/api/src/use-cases/ports/IEmailService.ts
  • apps/api/src/use-cases/reminders/SendFollowUpRemindersUseCase.ts
  • apps/web/src/routes/_authenticated/applications/$applicationId/index.tsx

Comment thread apps/api/.env.example
Comment on lines +26 to +27
FROM_EMAIL=noreply@yourdomain.com
FROM_NAME=Job Finder

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Quote environment variables that contain spaces.

As flagged by the static analysis tools, variables containing spaces should be enclosed in quotes to ensure compatibility across different environment variable parsers and deployment platforms.

🛠 Proposed fix
 FROM_EMAIL=noreply@yourdomain.com
-FROM_NAME=Job Finder
+FROM_NAME="Job Finder"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FROM_EMAIL=noreply@yourdomain.com
FROM_NAME=Job Finder
FROM_EMAIL=noreply@yourdomain.com
FROM_NAME="Job Finder"
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 27-27: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/.env.example` around lines 26 - 27, Update the FROM_NAME environment
variable in the example configuration to quote its space-containing value, while
leaving FROM_EMAIL unchanged.

Source: Linters/SAST tools

Comment on lines +7 to +14
const run = async () => {
try {
const { sendFollowUpRemindersUseCase } = fastify.diContainer.cradle;
await sendFollowUpRemindersUseCase.execute();
} catch {
// swallow — don't crash the server
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Log errors instead of silently swallowing them.

Silently catching and ignoring errors will make it very difficult to debug if the background reminder process starts failing (e.g., due to database connectivity issues or email service misconfigurations). Consider logging the error using Fastify's built-in logger.

💻 Proposed fix
   const run = async () => {
     try {
       const { sendFollowUpRemindersUseCase } = fastify.diContainer.cradle;
       await sendFollowUpRemindersUseCase.execute();
-    } catch {
-      // swallow — don't crash the server
+    } catch (error) {
+      fastify.log.error(error, 'Failed to execute follow-up reminders process');
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const run = async () => {
try {
const { sendFollowUpRemindersUseCase } = fastify.diContainer.cradle;
await sendFollowUpRemindersUseCase.execute();
} catch {
// swallow — don't crash the server
}
};
const run = async () => {
try {
const { sendFollowUpRemindersUseCase } = fastify.diContainer.cradle;
await sendFollowUpRemindersUseCase.execute();
} catch (error) {
fastify.log.error(error, 'Failed to execute follow-up reminders process');
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/http/plugins/reminders.plugin.ts` around lines 7 - 14, Update
the catch block in the run function to log the caught error through Fastify’s
built-in logger, while preserving the existing behavior of preventing background
reminder failures from crashing the server.

Comment on lines +73 to +81

async findDueForReminder(): Promise<Application[]> {
return this.inner.findDueForReminder();
}

async updateReminderSentAt(id: string, sentAt: Date): Promise<void> {
await this.inner.updateReminderSentAt(id, sentAt);
this.cache.delete(`apps:byId:${id}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Invalidate the list cache to prevent stale application data.

When a reminder is recorded, the Application object is modified. Failing to invalidate the list cache (apps:list:${userId}:) will result in users seeing stale reminder statuses when viewing their applications.

To enable updateReminderSentAt to clear the correct list cache, findDueForReminder must also populate the userIdByAppId map.

♻️ Proposed fix to keep the cache consistent
-  async findDueForReminder(): Promise<Application[]> {
-    return this.inner.findDueForReminder();
-  }
-
-  async updateReminderSentAt(id: string, sentAt: Date): Promise<void> {
-    await this.inner.updateReminderSentAt(id, sentAt);
-    this.cache.delete(`apps:byId:${id}`);
-  }
+  async findDueForReminder(): Promise<Application[]> {
+    const result = await this.inner.findDueForReminder();
+    for (const app of result) {
+      this.userIdByAppId.set(app.id, app.userId);
+    }
+    return result;
+  }
+
+  async updateReminderSentAt(id: string, sentAt: Date): Promise<void> {
+    await this.inner.updateReminderSentAt(id, sentAt);
+    this.cache.delete(`apps:byId:${id}`);
+    const userId = this.userIdByAppId.get(id);
+    if (userId) {
+      this.cache.deleteByPrefix(`apps:list:${userId}:`);
+    }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async findDueForReminder(): Promise<Application[]> {
return this.inner.findDueForReminder();
}
async updateReminderSentAt(id: string, sentAt: Date): Promise<void> {
await this.inner.updateReminderSentAt(id, sentAt);
this.cache.delete(`apps:byId:${id}`);
}
async findDueForReminder(): Promise<Application[]> {
const result = await this.inner.findDueForReminder();
for (const app of result) {
this.userIdByAppId.set(app.id, app.userId);
}
return result;
}
async updateReminderSentAt(id: string, sentAt: Date): Promise<void> {
await this.inner.updateReminderSentAt(id, sentAt);
this.cache.delete(`apps:byId:${id}`);
const userId = this.userIdByAppId.get(id);
if (userId) {
this.cache.deleteByPrefix(`apps:list:${userId}:`);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/infrastructure/db/repositories/CachedApplicationRepository.ts`
around lines 73 - 81, Update CachedApplicationRepository.findDueForReminder to
populate the userIdByAppId mapping for every returned application, then update
updateReminderSentAt to use that mapping to invalidate the corresponding
apps:list:${userId}: cache entry in addition to the existing by-id cache
deletion; remove or safely handle the mapping entry after invalidation as
appropriate.

Comment on lines +192 to +194
<p className="text-xs text-gray-400 mt-0.5">
Email reminder will be sent 24 h before this date.
</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid promising a reminder for past follow-ups.

This notice is rendered for any non-null followUpAt, including dates already in the past, although findDueForReminder() only selects dates from now through the next 24 hours. Also, the hourly scheduler sends within that window rather than exactly 24 hours before. Hide the notice for past dates and use wording such as “An email reminder is sent during the 24 hours before this date.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/routes/_authenticated/applications/`$applicationId/index.tsx
around lines 192 - 194, Update the follow-up reminder notice near the
`followUpAt` display to render only when the follow-up date is in the future,
while preserving the existing non-null check. Replace the “24 h before” wording
with language indicating the email reminder is sent during the 24 hours before
the date, consistent with the hourly scheduler behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant